Skip to main content

media_pp\elements\source\capture\windows/
wasapi_capture_source.rs

1use std::{
2    ffi::c_void,
3    ptr,
4    sync::Arc,
5    thread,
6    time::{Duration, Instant},
7};
8
9use crate::pp_log::{PpLog, pp_error, pp_info};
10use ffmpeg_next as ffmpeg;
11use thiserror::Error as ThisError;
12use windows::Win32::{
13    Media::Audio::{
14        AUDCLNT_BUFFERFLAGS_SILENT, AUDCLNT_E_DEVICE_INVALIDATED, AUDCLNT_SHAREMODE_SHARED,
15        AUDCLNT_STREAMFLAGS_LOOPBACK, IAudioCaptureClient, IAudioClient,
16    },
17    System::Com::{CLSCTX_ALL, CoTaskMemFree},
18};
19
20use crate::{
21    buffer::MediaBuffer,
22    bus::{Bus, BusEvent},
23    control::{self, ControlMsg, ControlOutcome, ControlReceiver, RequestKind},
24    element::{Element, ElementType, Source, SourceElement, element_pp_log},
25    error::Result,
26    pad::SrcPad,
27    platform::windows::wasapi::{
28        ComApartment, WasapiDevice, WasapiDeviceKind, list_devices as enumerate_wasapi_devices,
29        open_device, resolve_mix_format,
30    },
31    schedule::ActiveTimeline,
32};
33
34/// How long [`WasapiCaptureSource::run`] sleeps between checks of
35/// `GetNextPacketSize` — also bounds `Stop` latency, same reasoning as
36/// [`crate::elements::DxgiCaptureSource`]'s own `POLL_GRANULARITY`. Plain
37/// polling rather than `IAudioClient::SetEventHandle` + `WaitForSingleObject`:
38/// event-driven signaling is well documented as unreliable specifically
39/// for loopback capture (Microsoft's own WASAPILoopbackCapture sample
40/// polls for exactly this reason, rather than using
41/// `AUDCLNT_STREAMFLAGS_EVENTCALLBACK`). Using the same poll loop for
42/// `AudioCaptureMode::Microphone` too keeps one code path instead of
43/// branching between event-driven and polled just for a latency
44/// difference that doesn't matter at these timescales.
45const POLL_INTERVAL: Duration = Duration::from_millis(20);
46
47/// WASAPI shared-mode buffer size, in 100ns units (200ms) — comfortably
48/// larger than `POLL_INTERVAL` so this element's own wakeup cadence, not
49/// the device's ring buffer, is what bounds latency.
50const BUFFER_DURATION_100NS: i64 = 200 * 10_000;
51
52/// Errors specific to `WasapiCaptureSource`. Converts into the crate-wide
53/// `Error` via `?` (see [`crate::error::Error`]).
54#[derive(Debug, ThisError)]
55pub enum WasapiCaptureSourceError {
56    #[error("windows error: {0}")]
57    Windows(#[from] windows::core::Error),
58
59    /// `AUDCLNT_E_DEVICE_INVALIDATED` specifically, broken out of the
60    /// generic [`WasapiCaptureSourceError::Windows`] variant for the same
61    /// reason [`crate::elements::DxgiCaptureSourceError::AccessLost`] is:
62    /// the single most common *recoverable* failure (default device
63    /// changed, device unplugged, format changed) surfaces this way. Same
64    /// "fail fast, caller rebuilds a fresh one" contract
65    /// [`crate::elements::RtspSource`]/[`crate::elements::DxgiCaptureSource`]
66    /// already document: this element doesn't retry internally.
67    #[error("AUDCLNT_E_DEVICE_INVALIDATED — audio device needs to be reopened")]
68    DeviceInvalidated,
69
70    #[error("WasapiCaptureSource doesn't support seeking a live capture")]
71    SeekUnsupported,
72
73    #[error("unsupported WASAPI mix format: format_tag={format_tag}, bits_per_sample={bits}")]
74    UnsupportedMixFormat { format_tag: u32, bits: u16 },
75}
76
77/// Construction-time options for [`WasapiCaptureSource::open`].
78#[derive(Debug, Clone)]
79pub struct WasapiCaptureOptions {
80    /// Which endpoint to capture from — one entry out of
81    /// [`WasapiCaptureSource::list_devices`] (or hand-built, if the caller
82    /// already knows a device's id/kind some other way).
83    pub device: WasapiDevice,
84}
85
86/// Captures audio via WASAPI (`IAudioClient`/`IAudioCaptureClient`) —
87/// GStreamer's `wasapi2src` equivalent. One src pad, pushing
88/// `MediaBuffer::Audio` frames in the captured device's own native mix
89/// format/rate/channel count — no resampling. Same division of labor as
90/// [`crate::elements::DxgiCaptureSource`] emitting raw `Pixel::BGRA` and
91/// leaving conversion to a downstream [`crate::elements::Scaler`]: if
92/// something downstream needs a fixed sample rate/format, use
93/// [`crate::elements::AudioResampler`] rather than hiding conversion in
94/// this element.
95///
96/// Polls `IAudioCaptureClient::GetNextPacketSize` on a short fixed
97/// interval (`POLL_INTERVAL`) rather than waiting on a WASAPI-signaled
98/// event — see that constant's own docs on why event-driven mode isn't
99/// used here.
100///
101/// Emits continuously from the moment `run` starts, `pts` always in
102/// lockstep with wall-clock time — backed by real WASAPI data when it's
103/// available and synthesized silence otherwise (see
104/// `WasapiCaptureSource::fill_silence_gap`), since WASAPI itself
105/// delivers literally nothing whenever the render engine has no active
106/// session at all (e.g. nothing currently playing, for
107/// [`WasapiDeviceKind::Render`]). Without this, a quiet period would be a
108/// real gap in the audio timeline rather than silence, which would leave
109/// a downstream muxer/encoder with no way to keep audio and video in
110/// sync across it.
111///
112/// Every WASAPI object here is created by [`WasapiCaptureSource::open`] on
113/// its caller's thread, then actually driven by [`SourceElement::run`] on
114/// whichever thread [`crate::pipeline::Pipeline`] spawns for this source
115/// — a different thread in the normal case. COM requires every thread
116/// that touches an interface to have joined an apartment itself (even
117/// though the interfaces here are free-threaded/agile and can be handed
118/// across threads freely), so `run` makes its own `CoInitializeEx` call
119/// before touching anything, paired with `CoUninitialize` when it returns
120/// — the same two-`CoInitializeEx`-calls-per-object-lifetime pattern
121/// `cpal`'s own WASAPI backend uses. `open` also joins its caller's COM
122/// apartment while it creates the agile WASAPI interfaces, then balances
123/// that call before returning; the pipeline worker joins its own apartment
124/// independently in `run`.
125///
126/// Deliberately does **not** retry internally on
127/// `AUDCLNT_E_DEVICE_INVALIDATED` (default device changed, unplugged,
128/// format changed) — same "fail fast, caller rebuilds" contract as
129/// `DxgiCaptureSource`/`RtspSource`; watch for
130/// [`WasapiCaptureSourceError::DeviceInvalidated`] and call
131/// [`WasapiCaptureSource::open`] again.
132///
133/// Runs until `Stop` — never reaches `Eos` on its own, same as every other
134/// live source in this crate.
135pub struct WasapiCaptureSource {
136    pp_log: PpLog,
137    name: Arc<str>,
138    audio_client: IAudioClient,
139    capture_client: IAudioCaptureClient,
140    sample_rate: u32,
141    format: ffmpeg::format::Sample,
142    channel_layout: ffmpeg::ChannelLayout,
143    /// Cumulative sample count across every emitted frame — this
144    /// element's `pts` unit (see [`WasapiCaptureSource::time_base`]), same
145    /// "integer tick counter" convention every other source in this crate
146    /// uses.
147    samples_emitted: i64,
148    pad: SrcPad,
149}
150
151// SAFETY: every WASAPI/COM handle here is a `windows-rs` COM interface
152// wrapper — thread-safe to hand off (refcounting is interlocked, and
153// these specific interfaces are documented free-threaded/agile).
154// `&mut self` on every method that touches them already rules out
155// concurrent access from multiple threads — same reasoning
156// `DxgiCaptureSource` documents for its own `unsafe impl Send`.
157unsafe impl Send for WasapiCaptureSource {}
158
159impl WasapiCaptureSource {
160    /// Enumerates every currently-active audio endpoint — both `Render`
161    /// (playback) and `Capture` (recording) — as an [`WasapiDevice`] list a
162    /// caller can show in a picker UI and index/search into, then hand the
163    /// chosen entry straight to [`WasapiCaptureOptions::device`]. No
164    /// concept of "mode" to reason about beforehand: the picked device's
165    /// own [`WasapiDeviceKind`] is what tells `open` whether to use
166    /// loopback.
167    pub fn list_devices() -> std::result::Result<Vec<WasapiDevice>, WasapiCaptureSourceError> {
168        Ok(enumerate_wasapi_devices(None)?)
169    }
170
171    /// Opens `options.device` and starts a shared-mode WASAPI capture
172    /// session. Returns the element alongside the captured stream's
173    /// actual `(sample_rate, channels)` — what a caller needs to build a
174    /// matching downstream encoder/muxer, same pattern as
175    /// [`crate::elements::DxgiCaptureSource::open`] returning
176    /// `(width, height)`.
177    pub fn open(
178        name: impl Into<String>,
179        options: WasapiCaptureOptions,
180    ) -> std::result::Result<(Self, u32, u16), WasapiCaptureSourceError> {
181        let name: Arc<str> = name.into().into();
182        let pp_log = element_pp_log(ElementType::WasapiCaptureSource, &name, None);
183        let _apartment = ComApartment::new()?;
184
185        unsafe {
186            let device = open_device(&options.device.id)?;
187            let audio_client: IAudioClient = device.Activate(CLSCTX_ALL, None)?;
188
189            let mix_format = audio_client.GetMixFormat()?;
190            let audio_format = resolve_mix_format(mix_format).map_err(|error| {
191                WasapiCaptureSourceError::UnsupportedMixFormat {
192                    format_tag: error.format_tag,
193                    bits: error.bits,
194                }
195            })?;
196
197            let stream_flags = match options.device.kind {
198                WasapiDeviceKind::Render => AUDCLNT_STREAMFLAGS_LOOPBACK,
199                WasapiDeviceKind::Capture => 0,
200            };
201            let init_result = audio_client.Initialize(
202                AUDCLNT_SHAREMODE_SHARED,
203                stream_flags,
204                BUFFER_DURATION_100NS,
205                0,
206                mix_format,
207                None,
208            );
209            CoTaskMemFree(Some(mix_format as *const c_void));
210            init_result?;
211
212            let capture_client: IAudioCaptureClient = audio_client.GetService()?;
213
214            pp_info!(
215                pp_log: &pp_log,
216                "opened: device={:?} ({:?}), {}Hz, {} channel(s), format={:?}",
217                options.device.name,
218                options.device.kind,
219                audio_format.sample_rate,
220                audio_format.channels,
221                audio_format.sample_format
222            );
223            let pad = SrcPad::new(format!("{name}_src"));
224
225            Ok((
226                Self {
227                    name,
228                    pp_log,
229                    audio_client,
230                    capture_client,
231                    sample_rate: audio_format.sample_rate,
232                    format: audio_format.sample_format,
233                    channel_layout: audio_format.channel_layout(),
234                    samples_emitted: 0,
235                    pad,
236                },
237                audio_format.sample_rate,
238                audio_format.channels,
239            ))
240        }
241    }
242
243    /// The unit each emitted frame's `pts` is expressed in.
244    pub fn time_base(&self) -> ffmpeg::Rational {
245        ffmpeg::Rational::new(1, self.sample_rate as i32)
246    }
247
248    fn classify_error(&self, error: windows::core::Error) -> WasapiCaptureSourceError {
249        if error.code() == AUDCLNT_E_DEVICE_INVALIDATED {
250            WasapiCaptureSourceError::DeviceInvalidated
251        } else {
252            WasapiCaptureSourceError::Windows(error)
253        }
254    }
255
256    /// Wraps one WASAPI packet (`data`/`frames`/`flags` straight out of
257    /// [`IAudioCaptureClient::GetBuffer`]) into a fresh `ffmpeg::frame::Audio`
258    /// and stamps its `pts`. `AUDCLNT_BUFFERFLAGS_SILENT` (the device has
259    /// nothing real to report this tick, e.g. right after `Start`) or a
260    /// null `data` pointer both mean "emit silence" rather than reading
261    /// past the end of nothing.
262    fn build_frame(&mut self, data: *mut u8, frames: u32, flags: u32) -> ffmpeg::frame::Audio {
263        let mut frame =
264            ffmpeg::frame::Audio::new(self.format, frames as usize, self.channel_layout);
265        frame.set_rate(self.sample_rate);
266        // `frame.data_mut(0)`'s length is FFmpeg's own padded linesize,
267        // not necessarily `frames * channels * format.bytes()` exactly —
268        // only ever touch that tight amount (the same bound
269        // `frame.plane::<T>()` itself reads via `samples()`), never the
270        // destination's full length, or a WASAPI buffer exactly `frames`
271        // frames long could get read past its end.
272        let tight_bytes =
273            frames as usize * self.channel_layout.channels() as usize * self.format.bytes();
274        if data.is_null() || flags & (AUDCLNT_BUFFERFLAGS_SILENT.0 as u32) != 0 {
275            frame.data_mut(0)[..tight_bytes].fill(0);
276        } else {
277            unsafe {
278                ptr::copy_nonoverlapping(data, frame.data_mut(0).as_mut_ptr(), tight_bytes);
279            }
280        }
281        frame.set_pts(Some(self.samples_emitted));
282        self.samples_emitted += frames as i64;
283        frame
284    }
285
286    /// Pushes `frame` downstream, reporting (rather than dying on) a
287    /// failing `Sink` — same "drop this one buffer, keep going" contract
288    /// [`crate::elements::DxgiCaptureSource::run`]/[`crate::elements::TestVideoSource::run`]
289    /// give their own pushes.
290    fn push_frame(&mut self, frame: ffmpeg::frame::Audio, bus: &Bus) {
291        if let Err(error) = self.pad.push(MediaBuffer::Audio(Arc::new(frame))) {
292            bus.post(
293                &self.pp_log,
294                BusEvent::Error {
295                    element_type: ElementType::WasapiCaptureSource,
296                    name: self.name.clone(),
297                    error,
298                },
299            );
300        }
301    }
302
303    /// Synthesizes and pushes one silence frame covering however many
304    /// samples real WASAPI delivery has fallen behind `elapsed` — active
305    /// wall-clock time since `run_captured` started, already excluding
306    /// time spent frozen inside `Pause` (see
307    /// [`crate::schedule::ActiveTimeline`]; a raw `Instant::elapsed()`
308    /// would count a pause as a deficit to fill, making `Resume`
309    /// synthesize one giant silence frame covering the whole pause) — a
310    /// no-op (`deficit <= 0`) whenever real packets have kept up. WASAPI
311    /// delivers **zero** packets
312    /// whenever the render engine has no active session at all (as
313    /// opposed to an active-but-quiet session, which still delivers
314    /// `AUDCLNT_BUFFERFLAGS_SILENT`-flagged packets `build_frame` already
315    /// turns into silence) — without this, nothing plays on the system
316    /// would mean nothing at all comes out of this source, leaving a real
317    /// gap in the audio timeline exactly when a downstream muxer/encoder
318    /// needs `pts` to keep advancing to stay in sync with video. Backing
319    /// every gap with synthesized silence (rather than, say, stretching
320    /// the next real frame's `pts`) keeps `pts` a plain, always-accurate
321    /// sample count no matter which samples were real.
322    fn fill_silence_gap(&mut self, elapsed: Duration, bus: &Bus) {
323        let expected = (elapsed.as_secs_f64() * self.sample_rate as f64) as i64;
324        let deficit = expected - self.samples_emitted;
325        if deficit <= 0 {
326            return;
327        }
328        let mut frame =
329            ffmpeg::frame::Audio::new(self.format, deficit as usize, self.channel_layout);
330        frame.set_rate(self.sample_rate);
331        frame.data_mut(0).fill(0);
332        frame.set_pts(Some(self.samples_emitted));
333        self.samples_emitted += deficit;
334        self.push_frame(frame, bus);
335    }
336
337    /// Like [`crate::control::drain_control`], but drives the raw control
338    /// receiver and [`crate::control::apply_one`] directly (same reason
339    /// [`crate::elements::AppSource`] does — see `drain_control`'s own
340    /// docs) so it can bracket the blocking `Pause` wait with
341    /// `IAudioClient::Stop`/`Reset`/`Start`: without this, the capture
342    /// session keeps running the whole time this source is paused, with
343    /// nothing ever draining `IAudioCaptureClient::GetBuffer` to read it —
344    /// harmless in that WASAPI's shared-mode ring buffer just silently
345    /// overwrites whatever it can't hold (see `BUFFER_DURATION_100NS`), but
346    /// there's no reason to keep the device (and whatever driver-side work
347    /// backs it) actively capturing audio nobody is ever going to read.
348    /// `Reset` also flushes data already pending at the pause boundary, so
349    /// Resume cannot emit stale pre-pause packets as a short burst. These
350    /// device transitions happen before their synchronous control request
351    /// is acknowledged; failure is fatal because correct capture state can
352    /// no longer be guaranteed.
353    fn handle_control(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<ControlOutcome> {
354        let mut paused_for = Duration::ZERO;
355        while let Some((request, ack)) = control.try_recv() {
356            let RequestKind::Control(msg) = request else {
357                control::apply_finish(self, bus, &ack);
358                return Ok(ControlOutcome {
359                    stopped: true,
360                    paused_for,
361                });
362            };
363            if msg != ControlMsg::Pause {
364                if control::apply_one(self, bus, msg, &ack)? {
365                    return Ok(ControlOutcome {
366                        stopped: true,
367                        paused_for,
368                    });
369                }
370                continue;
371            }
372
373            // Include device shutdown and both synchronous downstream
374            // cascades in the frozen interval: this source produces no
375            // media during any of them.
376            let pause_start = Instant::now();
377            unsafe { self.audio_client.Stop() }.map_err(|error| self.classify_error(error))?;
378            // Stop freezes the stream but does not discard packets that
379            // were already pending. Reset flushes those packets so Resume
380            // cannot dump stale pre-pause audio in a short burst.
381            unsafe { self.audio_client.Reset() }.map_err(|error| self.classify_error(error))?;
382            control::apply_one(self, bus, msg, &ack)?;
383
384            loop {
385                let Some((paused_msg, paused_ack)) = control.recv() else {
386                    paused_for += pause_start.elapsed();
387                    return Ok(ControlOutcome {
388                        stopped: true,
389                        paused_for,
390                    });
391                };
392
393                let RequestKind::Control(paused_msg) = paused_msg else {
394                    control::apply_finish(self, bus, &paused_ack);
395                    paused_for += pause_start.elapsed();
396                    return Ok(ControlOutcome {
397                        stopped: true,
398                        paused_for,
399                    });
400                };
401
402                if paused_msg == ControlMsg::Resume {
403                    // Keep capture stopped until every downstream element
404                    // has resumed. Start the device and only then ack, so
405                    // the synchronous caller cannot observe a half-resumed
406                    // source and no audio accumulates during a slow cascade.
407                    control::apply_one_unacked(self, bus, paused_msg)?;
408                    unsafe { self.audio_client.Start() }
409                        .map_err(|error| self.classify_error(error))?;
410                    let _ = paused_ack.send(());
411                    paused_for += pause_start.elapsed();
412                    break;
413                }
414
415                if control::apply_one(self, bus, paused_msg, &paused_ack)? {
416                    paused_for += pause_start.elapsed();
417                    return Ok(ControlOutcome {
418                        stopped: true,
419                        paused_for,
420                    });
421                }
422                // A redundant Pause (or another one-shot control) was
423                // forwarded and acknowledged; remain frozen until Resume.
424            }
425        }
426        Ok(ControlOutcome {
427            stopped: false,
428            paused_for,
429        })
430    }
431
432    /// The main capture loop, run once COM has joined this thread's
433    /// apartment (see [`SourceElement::run`]) and the audio client has been
434    /// started. Drains every buffer WASAPI has ready on each
435    /// `POLL_INTERVAL` tick (`GetNextPacketSize` returning `0` means
436    /// caught up), pushing one `MediaBuffer::Audio` per packet, then tops
437    /// up with synthesized silence (see [`WasapiCaptureSource::fill_silence_gap`])
438    /// so `pts` keeps advancing with wall-clock time even across a tick
439    /// where WASAPI delivered nothing at all.
440    fn run_captured(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
441        let mut timeline = ActiveTimeline::new(Instant::now());
442        loop {
443            let outcome = self.handle_control(control, bus)?;
444            if outcome.stopped {
445                pp_info!(self, "stopped");
446                return Ok(());
447            }
448            timeline.account_pause(outcome.paused_for);
449
450            thread::sleep(POLL_INTERVAL);
451
452            loop {
453                let packet_size = match unsafe { self.capture_client.GetNextPacketSize() } {
454                    Ok(size) => size,
455                    Err(error) => return Err(self.classify_error(error).into()),
456                };
457                if packet_size == 0 {
458                    break;
459                }
460
461                let mut data: *mut u8 = ptr::null_mut();
462                let mut frames_available = 0u32;
463                let mut flags = 0u32;
464                if let Err(error) = unsafe {
465                    self.capture_client.GetBuffer(
466                        &mut data,
467                        &mut frames_available,
468                        &mut flags,
469                        None,
470                        None,
471                    )
472                } {
473                    return Err(self.classify_error(error).into());
474                }
475
476                let frame = self.build_frame(data, frames_available, flags);
477                if let Err(error) = unsafe { self.capture_client.ReleaseBuffer(frames_available) } {
478                    return Err(self.classify_error(error).into());
479                }
480
481                self.push_frame(frame, bus);
482            }
483
484            self.fill_silence_gap(timeline.elapsed(Instant::now()), bus);
485        }
486    }
487}
488
489impl Element for WasapiCaptureSource {
490    fn name(&self) -> Arc<str> {
491        self.name.clone()
492    }
493
494    fn element_type(&self) -> ElementType {
495        ElementType::WasapiCaptureSource
496    }
497
498    fn pp_log(&self) -> &PpLog {
499        &self.pp_log
500    }
501
502    fn pp_log_mut(&mut self) -> &mut PpLog {
503        &mut self.pp_log
504    }
505}
506
507impl Source for WasapiCaptureSource {
508    fn src_pads(&mut self) -> &mut [SrcPad] {
509        std::slice::from_mut(&mut self.pad)
510    }
511}
512
513impl SourceElement for WasapiCaptureSource {
514    fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
515        pp_info!(self, "started");
516
517        let _apartment = ComApartment::new().map_err(WasapiCaptureSourceError::from)?;
518
519        if let Err(error) = unsafe { self.audio_client.Start() } {
520            return Err(self.classify_error(error).into());
521        }
522
523        let result = self.run_captured(control, bus);
524
525        if let Err(error) = unsafe { self.audio_client.Stop() } {
526            pp_error!(self, "Stop failed: {error}");
527        }
528        result
529    }
530
531    fn seek(&mut self, _target: std::time::Duration) -> Result<std::time::Duration> {
532        Err(WasapiCaptureSourceError::SeekUnsupported.into())
533    }
534}